//
//  UIScrollView+TPKeyboardAvoidingAdditions.m
//  TPKeyboardAvoidingSample
//
//  Created by Michael Tyson on 30/09/2013.
//  Copyright 2013 A Tasty Pixel. All rights reserved.
//

#import "UIScrollView+TPKeyboardAvoidingAdditions.h"
#import "TPKeyboardAvoidingScrollView.h"
#import <objc/runtime.h>

static const CGFloat kCalculatedContentPadding = 10;
static const CGFloat kMinimumScrollOffsetPadding = 20;

static const int kStateKey;

#define _UIKeyboardFrameEndUserInfoKey                                                                                 \
	(&UIKeyboardFrameEndUserInfoKey != NULL ? UIKeyboardFrameEndUserInfoKey : @"UIKeyboardBoundsUserInfoKey")

@interface TPKeyboardAvoidingState : NSObject
@property(nonatomic, assign) UIEdgeInsets priorInset;
@property(nonatomic, assign) UIEdgeInsets priorScrollIndicatorInsets;
@property(nonatomic, assign) BOOL keyboardVisible;
@property(nonatomic, assign) CGRect keyboardRect;
@property(nonatomic, assign) CGSize priorContentSize;
@end

@implementation UIScrollView (TPKeyboardAvoidingAdditions)

- (TPKeyboardAvoidingState *)keyboardAvoidingState {
	TPKeyboardAvoidingState *state = objc_getAssociatedObject(self, &kStateKey);
	if (!state) {
		state = [[TPKeyboardAvoidingState alloc] init];
		objc_setAssociatedObject(self, &kStateKey, state, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
#if !__has_feature(objc_arc)
		[state release];
#endif
	}
	return state;
}

- (void)TPKeyboardAvoiding_keyboardWillShow:(NSNotification *)notification {
	TPKeyboardAvoidingState *state = self.keyboardAvoidingState;

	if (state.keyboardVisible) {
		return;
	}

	UIView *firstResponder = [self TPKeyboardAvoiding_findFirstResponderBeneathView:self];

	state.keyboardRect =
		[self convertRect:[[[notification userInfo] objectForKey:_UIKeyboardFrameEndUserInfoKey] CGRectValue]
				 fromView:nil];
	state.keyboardVisible = YES;
	state.priorInset = self.contentInset;
	state.priorScrollIndicatorInsets = self.scrollIndicatorInsets;

	if ([self isKindOfClass:[TPKeyboardAvoidingScrollView class]]) {
		state.priorContentSize = self.contentSize;

		if (CGSizeEqualToSize(self.contentSize, CGSizeZero)) {
			// Set the content size, if it's not set. Do not set content size explicitly if auto-layout
			// is being used to manage subviews
			self.contentSize = [self TPKeyboardAvoiding_calculatedContentSizeFromSubviewFrames];
		}
	}

	// Shrink view's inset by the keyboard's height, and scroll to show the text field/view being edited
	[UIView beginAnimations:nil context:NULL];
	[UIView setAnimationCurve:[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];
	[UIView setAnimationDuration:[[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey]
									 floatValue]];

	self.contentInset = [self TPKeyboardAvoiding_contentInsetForKeyboard];

	if (firstResponder) {
		CGFloat viewableHeight = self.bounds.size.height - self.contentInset.top - self.contentInset.bottom;
		[self setContentOffset:CGPointMake(self.contentOffset.x,
										   [self TPKeyboardAvoiding_idealOffsetForView:firstResponder
																 withViewingAreaHeight:viewableHeight])
					  animated:NO];
	}

	self.scrollIndicatorInsets = self.contentInset;

	[UIView commitAnimations];
}

- (void)TPKeyboardAvoiding_keyboardWillHide:(NSNotification *)notification {
	TPKeyboardAvoidingState *state = self.keyboardAvoidingState;

	if (!state.keyboardVisible) {
		return;
	}

	state.keyboardRect = CGRectZero;
	state.keyboardVisible = NO;

	// Restore dimensions to prior size
	[UIView beginAnimations:nil context:NULL];
	[UIView setAnimationCurve:[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];
	[UIView setAnimationDuration:[[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey]
									 floatValue]];

	if ([self isKindOfClass:[TPKeyboardAvoidingScrollView class]]) {
		self.contentSize = state.priorContentSize;
	}

	self.contentInset = state.priorInset;
	self.scrollIndicatorInsets = state.priorScrollIndicatorInsets;
	[UIView commitAnimations];
}

- (void)TPKeyboardAvoiding_updateContentInset {
	TPKeyboardAvoidingState *state = self.keyboardAvoidingState;
	if (state.keyboardVisible) {
		self.contentInset = [self TPKeyboardAvoiding_contentInsetForKeyboard];
	}
}

- (void)TPKeyboardAvoiding_updateFromContentSizeChange {
	TPKeyboardAvoidingState *state = self.keyboardAvoidingState;
	if (state.keyboardVisible) {
		state.priorContentSize = self.contentSize;
		self.contentInset = [self TPKeyboardAvoiding_contentInsetForKeyboard];
	}
}

#pragma mark - Utilities

- (BOOL)TPKeyboardAvoiding_focusNextTextField {
	UIView *firstResponder = [self TPKeyboardAvoiding_findFirstResponderBeneathView:self];
	if (!firstResponder) {
		return NO;
	}

	CGFloat minY = CGFLOAT_MAX;
	UIView *view = nil;
	[self TPKeyboardAvoiding_findTextFieldAfterTextField:firstResponder beneathView:self minY:&minY foundView:&view];

	if (view) {
		[view performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0.0];
		return YES;
	}

	return NO;
}

- (void)TPKeyboardAvoiding_scrollToActiveTextField {
	TPKeyboardAvoidingState *state = self.keyboardAvoidingState;

	if (!state.keyboardVisible)
		return;

	CGFloat visibleSpace = self.bounds.size.height - self.contentInset.top - self.contentInset.bottom;

	CGPoint idealOffset = CGPointMake(
		0, [self TPKeyboardAvoiding_idealOffsetForView:[self TPKeyboardAvoiding_findFirstResponderBeneathView:self]
								 withViewingAreaHeight:visibleSpace]);

	// Ordinarily we'd use -setContentOffset:animated:YES here, but it does not appear to
	// scroll to the desired content offset. So we wrap in our own animation block.
	[UIView animateWithDuration:0.25
					 animations:^{
					   [self setContentOffset:idealOffset animated:NO];
					 }];
}

#pragma mark - Helpers

- (UIView *)TPKeyboardAvoiding_findFirstResponderBeneathView:(UIView *)view {
	// Search recursively for first responder
	for (UIView *childView in view.subviews) {
		if ([childView respondsToSelector:@selector(isFirstResponder)] && [childView isFirstResponder])
			return childView;
		UIView *result = [self TPKeyboardAvoiding_findFirstResponderBeneathView:childView];
		if (result)
			return result;
	}
	return nil;
}

- (void)TPKeyboardAvoiding_findTextFieldAfterTextField:(UIView *)priorTextField
										   beneathView:(UIView *)view
												  minY:(CGFloat *)minY
											 foundView:(UIView **)foundView {
	// Search recursively for text field or text view below priorTextField
	CGFloat priorFieldOffset = CGRectGetMinY([self convertRect:priorTextField.frame fromView:priorTextField.superview]);
	for (UIView *childView in view.subviews) {
		if (childView.hidden)
			continue;
		if (([childView isKindOfClass:[UITextField class]] || [childView isKindOfClass:[UITextView class]]) &&
			childView.isUserInteractionEnabled) {
			CGRect frame = [self convertRect:childView.frame fromView:view];
			if (childView != priorTextField && CGRectGetMinY(frame) >= priorFieldOffset &&
				CGRectGetMinY(frame) < *minY &&
				!(frame.origin.y == priorTextField.frame.origin.y && frame.origin.x < priorTextField.frame.origin.x)) {
				*minY = CGRectGetMinY(frame);
				*foundView = childView;
			}
		} else {
			[self TPKeyboardAvoiding_findTextFieldAfterTextField:priorTextField
													 beneathView:childView
															minY:minY
													   foundView:foundView];
		}
	}
}

- (void)TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:(UIView *)view {
	for (UIView *childView in view.subviews) {
		if (([childView isKindOfClass:[UITextField class]] || [childView isKindOfClass:[UITextView class]])) {
			[self TPKeyboardAvoiding_initializeView:childView];
		} else {
			[self TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:childView];
		}
	}
}

- (CGSize)TPKeyboardAvoiding_calculatedContentSizeFromSubviewFrames {

	BOOL wasShowingVerticalScrollIndicator = self.showsVerticalScrollIndicator;
	BOOL wasShowingHorizontalScrollIndicator = self.showsHorizontalScrollIndicator;

	self.showsVerticalScrollIndicator = NO;
	self.showsHorizontalScrollIndicator = NO;

	CGRect rect = CGRectZero;
	for (UIView *view in self.subviews) {
		rect = CGRectUnion(rect, view.frame);
	}
	rect.size.height += kCalculatedContentPadding;

	self.showsVerticalScrollIndicator = wasShowingVerticalScrollIndicator;
	self.showsHorizontalScrollIndicator = wasShowingHorizontalScrollIndicator;

	return rect.size;
}

- (UIEdgeInsets)TPKeyboardAvoiding_contentInsetForKeyboard {
	TPKeyboardAvoidingState *state = self.keyboardAvoidingState;
	UIEdgeInsets newInset = self.contentInset;
	CGRect keyboardRect = state.keyboardRect;
	newInset.bottom = keyboardRect.size.height - (CGRectGetMaxY(keyboardRect) - CGRectGetMaxY(self.bounds));
	return newInset;
}

- (CGFloat)TPKeyboardAvoiding_idealOffsetForView:(UIView *)view withViewingAreaHeight:(CGFloat)viewAreaHeight {
	CGSize contentSize = self.contentSize;
	CGFloat offset = 0.0;

	CGRect subviewRect = [view convertRect:view.bounds toView:self];

	// Attempt to center the subview in the visible space, but if that means there will be less than
	// kMinimumScrollOffsetPadding
	// pixels above the view, then substitute kMinimumScrollOffsetPadding
	CGFloat padding = (viewAreaHeight - subviewRect.size.height) / 2;
	if (padding < kMinimumScrollOffsetPadding) {
		padding = kMinimumScrollOffsetPadding;
	}

	// Ideal offset places the subview rectangle origin "padding" points from the top of the scrollview.
	// If there is a top contentInset, also compensate for this so that subviewRect will not be placed under
	// things like navigation bars.
	offset = subviewRect.origin.y - padding - self.contentInset.top;

	// Constrain the new contentOffset so we can't scroll past the bottom. Note that we don't take the bottom
	// inset into account, as this is manipulated to make space for the keyboard.
	if (offset > (contentSize.height - viewAreaHeight)) {
		offset = contentSize.height - viewAreaHeight;
	}

	// Constrain the new contentOffset so we can't scroll past the top, taking contentInsets into account
	if (offset < -self.contentInset.top) {
		offset = -self.contentInset.top;
	}

	return offset;
}

- (void)TPKeyboardAvoiding_initializeView:(UIView *)view {
	if ([view isKindOfClass:[UITextField class]] && ((UITextField *)view).returnKeyType == UIReturnKeyDefault &&
		(![(id)view delegate] || [(UIScrollView *)view delegate] == (id<UIScrollViewDelegate>)self)) {
		[(UIScrollView *)view setDelegate:(id<UIScrollViewDelegate>)self];
		UIView *otherView = nil;
		CGFloat minY = CGFLOAT_MAX;
		[self TPKeyboardAvoiding_findTextFieldAfterTextField:view beneathView:self minY:&minY foundView:&otherView];

		if (otherView) {
			((UITextField *)view).returnKeyType = UIReturnKeyNext;
		} else {
			((UITextField *)view).returnKeyType = UIReturnKeyDone;
		}
	}
}

@end

@implementation TPKeyboardAvoidingState
@end
